namespace Game {
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.IO;

    using UnityEngine;

    using UA = UnityEngine.Application;

    public class Updater : MonoBehaviour {
        public enum Step {
            Unpacking,
            Checking,
            Calculating,
            Downloading,
            Error,
            Done,
        }
        public enum ErrorCode {
            None,
            NetworkNotAvailable,
            NetworkNotStable,
            LocalBroken,
            DownloadFailed,
        }

        public static Step CurrStep = Step.Checking;

        public static ErrorCode Error = ErrorCode.None;

        public static float Progress = 0.0f;

        public static Updater Ins = null;

        public static IEnumerator OnInit(Action<AssetBundleManifest> onFinished) {
            if (Ins != null) {
                OnShutdown(); yield break;
            }

            CurrStep = Step.Checking; Error = ErrorCode.None; Progress = 0.0f;

            yield return DoUpdate(onFinished);
        }

        public static void Clear() {
            remoteManifestBundle = null;

            local = null; remote = null;
        }

        public static void OnShutdown() {
            if (Ins == null) return;
            Destroy(Ins); Ins = null;
        }

        private static IEnumerator DoUpdate(Action<AssetBundleManifest> onFinished) {
            yield return PackageInfo.OnInit();

            if (PlayerSetting.UnpackVersion != PackageInfo.Ins.Version) {
                yield return CopyBundles();
            } else {
                yield return LoadLocalManifest();
            }

            if (!PackageInfo.Ins.EnableUpdate) {
                CurrStep = Step.Done; Error = ErrorCode.None; Progress = 1.0f;

                if (onFinished != null) onFinished(Updater.local);
                yield break;
            }

            Dir.SetUpdateUrl(PackageInfo.Ins.UpdateUrl);

            if (UA.internetReachability == NetworkReachability.NotReachable) {
                SetError(ErrorCode.NetworkNotAvailable);
                yield break;
            } else if (UA.internetReachability == NetworkReachability.ReachableViaCarrierDataNetwork) {
                SetError(ErrorCode.NetworkNotStable);
                yield break;
            }

            yield return DownloadRemoteManifest();

            CurrStep = Step.Calculating; Progress = 0.1f;

            string[] cur = remote.GetAllAssetBundles();
            var local = new List<string>(); local.AddRange(Utils.GetFiles(Dir.Storage, ".u"));
            var needUpdate = new List<string>();

            for (int i = 0; i < cur.Length; ++i) {
                string name = cur[i];

                if (local.Contains(name) && Updater.local.GetAssetBundleHash(name) != remote.GetAssetBundleHash(name)) needUpdate.Add(name);
            }

            Progress = 0.2f;
            if (needUpdate.Count > 0) {
                float step = 0.8f / needUpdate.Count;
                foreach (var name in needUpdate) {
                    WWW one = new WWW(Dir.UpdateUrl + name); yield return one;

                    if (one.error != null) {
                        Logger.Error("Updater failed to fetch remote AssetBundle: {0}. {1}", name, one.error);
                        SetError(ErrorCode.DownloadFailed);
                        yield break;
                    } else {
                        Logger.Info("Updater download {0} ... [OK]", one.url);
                    }

                    Utils.WriteBundle2Local(name, one.bytes); one.Dispose();

                    Progress += step;
                }
            }

            Utils.WriteBundle2Local(Dir.Platform, remoteManifestBundle);

            CurrStep = Step.Done; Progress = 1.0f; Error = ErrorCode.None;

            if (onFinished != null) onFinished(Updater.local);
        }

        private static IEnumerator CopyBundles() {
            CurrStep = Step.Unpacking; Progress = 0.0f;

            WWW www = new WWW(Dir.Streaming + Dir.Platform); yield return www;
            if (www.error != null) {
                Logger.Error("Read build-in AssetBundleManifest from streaming asset failed.");
                SetError(ErrorCode.LocalBroken);
                yield break;
            }

            Utils.WriteBundle2Local(Dir.Platform, www.bytes);

            var req = AssetBundle.LoadFromMemoryAsync(www.bytes); yield return req;

            local = req.assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
            req.assetBundle.Unload(false);
            www.Dispose();


            List<string> bundles = PackageInfo.Ins.PackedBundles;
            if (bundles.Count <= 0) yield break;

            float step = 1.0f / bundles.Count;
            foreach (var b in bundles) {
                WWW one = new WWW(Dir.Streaming + b); yield return one;

                if (one.error != null) {
                    Logger.Error("Unpack bundle {0} ... [Failed]. {1}", b, one.error);
                    SetError(ErrorCode.DownloadFailed);
                    yield break;
                } else {
                    Logger.Info("Unpack bundle {0} ... [OK]", b);
                }

                Utils.WriteBundle2Local(b, one.bytes); one.Dispose();

                Progress += step;

                PlayerSetting.UnpackVersion = PackageInfo.Ins.Version;
            }
        }

        private static IEnumerator LoadLocalManifest() {
            var req = AssetBundle.LoadFromFileAsync(Dir.Storage + Dir.Platform); yield return req;

            if (!req.isDone || req.assetBundle == null) {
                Logger.Error("Read local bundle information failed.");
                yield break;
            }

            local = req.assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
            req.assetBundle.Unload(false);
        }

        private static IEnumerator DownloadRemoteManifest() {
            CurrStep = Step.Checking; Progress = 0.0f;

            string url = Dir.UpdateUrl + Dir.Platform;
            WWW www = new WWW(url); yield return www;

            if (www.error != null) {
                Logger.Error("Updater failed to fetch remote AssetBundleManifest {0}. {1}", url, www.error);
                SetError(ErrorCode.DownloadFailed);
                yield break;
            }

            remoteManifestBundle = www.bytes; www.Dispose();
            var req = AssetBundle.LoadFromMemoryAsync(remoteManifestBundle); yield return req;

            if (!req.isDone) {
                Logger.Error("Updater faild to parse remote AssetBundleManifest.");
                SetError(ErrorCode.DownloadFailed);
                yield break;
            }

            remote = req.assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
        }

        private static void SetError(ErrorCode code) {
            CurrStep = Step.Error; Error = code;
        }

        private static byte[] remoteManifestBundle;
        private static AssetBundleManifest local;
        private static AssetBundleManifest remote;
    }
}